/*
 * Copyright (C) 2010, Funcom AS, All Rights Reserved
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 *  @authors
 *  Svein Gunnar Larsen <sveingunnarlarsen@gmail.com>
 *  Erik Thune Lund <culex.eriklund@gmail.com>
 *  Michel Krohn-Dale <kronicz@gmail.com>
 */

package com.funcom.peeler.properties;

import com.funcom.peeler.UpdateCont;
import com.funcom.peeler.WorkBench;
import com.funcom.peeler.action.Change;
import com.funcom.peeler.action.Delete;
import com.funcom.peeler.action.MultiChange;
import com.funcom.peeler.adapter.KeyAndMouseAdapter;
import com.funcom.peeler.list.ComponentRowModel;
import com.funcom.peeler.list.ListNode;
import com.funcom.peeler.peelable.PeelCanvasWindow;
import com.funcom.peeler.peelable.PeelComponent;
import com.funcom.peeler.peelable.PeelContainer;
import org.netbeans.swing.outline.DefaultOutlineModel;
import org.netbeans.swing.outline.Outline;
import org.netbeans.swing.outline.OutlineModel;

import javax.swing.*;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreeNode;
import javax.swing.tree.TreePath;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.Set;


/**
 * This class updates the component list. It is quite big and is not very elegantly implemented.
 * If you have some better solution to the updating, you are welcome to do it.
 */

public class ComponentList extends UpdateableProperties {

    // A copy of the component list from the current active workbench.
    private Set<PeelComponent> allComp;
    // A copy af the allComp list when the last populating of the list took place.
    private Set<PeelComponent> allPrevious;
    // A copy of the selected components from the workbench.
    private Set<PeelComponent> selectedComps;

    // Components that should be added to the list.
    private Set<PeelComponent> newComponents     = new HashSet<PeelComponent>();
    // Components that should be deleted from the list.
    private Set<PeelComponent> deletedComponents = new HashSet<PeelComponent>();

    // A Set of list nodes that has a change in their hide or lock state.
    private Set<ListNode> hideOrLock = new HashSet<ListNode>();

    // Classes needed for the list structure.
    private Outline outline;
    private OutlineModel outlineModel;
    private DefaultTreeModel listModel;
    private DefaultListSelectionModel selectionModel;

    private ListNode root = null;
    private WorkBench workbench = null;

    private boolean updateFromList  = false;
    private boolean updateHideLock  = false;
    private boolean newWorkbench    = false;
    private boolean newHierarchy    = false;

    // Constructor initializing the list.
    public ComponentList(JScrollPane componentPane) {

        root = new ListNode();

        listModel = new DefaultTreeModel(root);
        outlineModel = DefaultOutlineModel.createOutlineModel(listModel, new ComponentRowModel());

        outline = new Outline();
        outline.setRootVisible(true);
        outline.setModel(outlineModel);

        selectionModel = new DefaultListSelectionModel();
        outline.setSelectionModel(selectionModel);

        initListeners();

        componentPane.setViewportView(outline);
    }

    public void update() {
        populateComponentList();
    }

    /**
     * The needUpdate function is derived from the abstract class UpdateableProperties. It checks for changes
     * in the state of the workbench and sets the wantupdate to true if there is a change that requires the
     * list to update.
     * @param workBench is used to check if it is a new workbench and reloads the list if it is needed.
     * @param allComp checks against the allPrevious for new components.
     * @param selectedComps used to highlight components in the list.
     * @param main_selected used to expand the list to the component that is selected.
     * @param thisFrame used to check if there is a new- or delete event from the updatecontroller.
     */
    public void needUpdate( WorkBench workBench,
                            Set<PeelComponent> allComp,
                            Set<PeelComponent> selectedComps,
                            PeelComponent main_selected,
                            PropertiesFrame thisFrame ) {

        this.allComp = allComp;
        selectedComp = main_selected;

        if (this.workbench != workBench) {
            this.workbench = workBench;
            newWorkbench = true;
            wantUpdate   = true;
            return;
        }

        if (thisFrame.getHierarchyChange()) {
            newHierarchy = true;
            wantUpdate   = true;
        }

        if (!selectedComps.equals( this.selectedComps )) {
            this.selectedComps = selectedComps;
            wantUpdate = true;
        }

        // If the component list from the previous update don't equal the now list we need to do an update.
        if (!allComp.equals( allPrevious ) && thisFrame.isNewOrDeleted()) {
            newOrDeletedComponents();
            thisFrame.setNewOrDelete(false);            
        }

        if (updateHideLock) {
            hideAndLock();
            updateHideLock = false;
        }

        if (updateFromList || newHierarchy) {
            wantUpdate = true;
        }

        isNameChange();

        if (thisFrame.getHierarchyChange()) {
            assertList();
            thisFrame.setHierarchyChange(false);
        }
    }

    /**
     * Applies hide or lock on functions.
     * @param component the component that needs a change.
     * @param updater links to the frame where the update runs
     */
    public void applyChange( PeelComponent component, PropertiesFrame updater) {

        switch (change) {
            case HIDE_LOCK: {

                for (ListNode node : hideOrLock) {

                    UpdateCont.get().invalidateComponent( node.getComponent(), false, null);

                    node.getComponent().setHidden( node.getHide() );
                    node.getComponent().setLocked( node.getLock() );
                }

                hideOrLock.clear();

                updateSelectedFromList();
                break;
            }
        }
    }

    public void clear() {
        root.setComponent(null);
        root.removeAllChildren();
    }

    public void populateComponentList() {

        allPrevious = new HashSet<PeelComponent>(allComp);

        if (newWorkbench || newHierarchy) {

            reloadList();

            newWorkbench = false;
            newHierarchy = false;

            deletedComponents.clear();
            newComponents.clear();

            assertList();

            listModel.reload();

            if (selectedComp != null) {
                updateSelectedFromBench();
            }
            
            return;
        }
        
        if (!deletedComponents.isEmpty()) {
            for (PeelComponent aComp : deletedComponents) {
                removeComponentFromTree( aComp );
            }
            deletedComponents.clear();
        }

        if (!newComponents.isEmpty()) {
            for (PeelComponent aComp : newComponents) {
                addComponentToTree( aComp );
            }
            newComponents.clear();
        }

        if (!updateFromList) {
            updateSelectedFromBench();
        } else {
            updateSelectedFromList();
            updateFromList = false;
        }

        updateName();

        assertList();
    }

    public void isNameChange() {
        for (PeelComponent aComp : allComp) {
            String prev_name = (String)aComp.getComponent().getProperty("name_prop");
            if (prev_name == null) continue;
            if (!prev_name.equals(aComp.getComponent().getName())) {
                wantUpdate = true;
            }
        }
    }

    public void updateName() {
        for (PeelComponent aComp : allComp) {
            aComp.getComponent().setProperty("name_prop", aComp.getComponent().getName());
        }
    }

    /**
     * This method is used extensively throughout the class. This method should optimally not exist. It runs through
     * all the nodes and checks if there is any duplicates or nodes that shouldn't be in the list, and removes them if
     * there is. The fact that this method is necessary to makes things work, is and indication that the implementation
     * of the updating is not what it should be.
     */
    private void assertList() {

        if (root == null) return;

        Enumeration<ListNode> rootEnum = root.breadthFirstEnumeration();
        Set< PeelComponent > inList = new HashSet< PeelComponent >();

        int i = 0;
        while (rootEnum.hasMoreElements()) {            
            ListNode node = rootEnum.nextElement();

            if (allComp.contains(node.getComponent())) {

                if (inList.contains( node.getComponent() )) {
                    ListNode parent;
                    parent = (ListNode)node.getParent();
                    node.removeFromParent();
                    listModel.reload( parent );
                    wantUpdate = true;
                } else {
                    i++;
                    inList.add( node.getComponent() );
                }
            } else {
                ListNode parent;
                parent = (ListNode)node.getParent();
                node.removeFromParent();
                listModel.reload( parent );
                wantUpdate = true;
            }
        }        
    }

    /**
     * Clears the whole list and reloads all the components from the active workbench.
     */
    private void reloadList() {

        clear();

        for (PeelComponent aComp : allComp) {
            if (aComp.getComponent().getName().equals("workspace_window")) {
                root.setComponent(aComp);
            }
            if (aComp.getComponent().getName().equals("canvas_window")) {
                root.add(new ListNode(aComp));
            }
        }

        for (PeelComponent aComp : allComp) {
            if ((aComp.getComponent().getName().equals("workspace_window"))
                || (aComp.getComponent().getName().equals("canvas_window"))) {
            } else {
                addComponentToTree( aComp );
            }
        }
    }

    /**
     * Adds components to the new or deleted sets. And removes or adds respectively in the populate method.
     */
    private void newOrDeletedComponents() {
        // If the allComp is bigger we need to add.
        if (allPrevious.size() < allComp.size()) {
            for (PeelComponent aComp : allComp) {
                if (!allPrevious.contains(aComp)) {
                    wantUpdate = true;
                    newComponents.add( aComp );
                }
            }
        // If the allComp is smaller we need to delete.
        } else {
            for (PeelComponent aComp : allPrevious) {
                if (!allComp.contains(aComp)) {
                    deletedComponents.add( aComp );
                    wantUpdate = true;
                }
            }
        }
    }

    /**
     * Updates what is selected in the list, from what is currently selected in the bench.
     */
    private void updateSelectedFromBench() {
        outline.clearSelection();

        for (PeelComponent aComp : selectedComps) {
            ListNode node;
            Enumeration rootEnumeration = root.breadthFirstEnumeration();

            while (rootEnumeration.hasMoreElements()) {
                node = (ListNode)rootEnumeration.nextElement();

                if (node.getComponent() == aComp) {
                    searchNodeAndExpand( node );
                }
            }
        }
    }

    /**
     *  Updates what is selected in the bench from what is currently selected in the list.
     */
    private void updateSelectedFromList() {

        int[] selected = outline.getSelectedRows();
        ListNode[] selectedNodes = new ListNode[ selected.length ];

        for (int i = 0 ; i < selected.length; i++) {
            selectedNodes[i] = (ListNode) outline.getValueAt(selected[i], 0);
        }

        final ListNode[] temp = selectedNodes;

        workbench.getBui().execute(new WorkBench.Task() {
            public void run() {

                workbench.getBui().clearSelections();

                for (ListNode aNode : temp) {
                    workbench.getBui().select( aNode.getComponent() );
                }
            }
        });
    }

    /**
     * Searches for the specified node and selects it in the list.
     * @param nodeToSelect adds a selection color in the list to this node.
     */
    private void searchNodeAndExpand(ListNode nodeToSelect) {
        TreeNode[] path = search ((ListNode)outlineModel.getRoot(), nodeToSelect );

        if (path != null) {
            TreePath treePath = new TreePath (path);

            outline.expandPath( treePath );

            for (int i = 0; i < outline.getRowCount(); i++) {

                ListNode select = (ListNode)outline.getValueAt(i, 0);

                if (select.getComponent() == nodeToSelect.getComponent()) {
                    selectionModel.addSelectionInterval(i, i);
                }
            }
        }
    }

    /**
     * Used by search node and expand method to find the selected node in the list.
     * The list should expand to this node and select it.
     * @param node the root node int the tree structure.
     * @param nodeToSelect
     * @return
     */
    private TreeNode[] search (ListNode node, ListNode nodeToSelect ) {
        TreeNode[] path = null;

        if (node.getComponent() == nodeToSelect.getComponent()) {

            path = listModel.getPathToRoot( node );

        } else {
            int i = 0;
            int n = listModel.getChildCount (node);

            while ((i < n) && (path == null)) {
                path = search ((ListNode) listModel.getChild (node, i), nodeToSelect);
                i ++;
            }
        }
        return path;
    }


    private boolean addComponentToTree( PeelComponent component ) {

        ListNode node;
        Enumeration rootEnumeration = root.breadthFirstEnumeration();

        while (rootEnumeration.hasMoreElements()) {

            node = (ListNode)rootEnumeration.nextElement();

            if (component.getParent() == node.getComponent()) {
                ListNode child = new ListNode( component );
                node.add( child );

                assertHideAndLock( child );

                if (component instanceof PeelContainer) {
                    addChildren( child );
                }
                listModel.reload( node );
                return true;
            }
        }
        return false;
    }

    private void addChildren( ListNode parent ) {

        int compCount = ((PeelContainer)parent.getComponent() ).getComponentCount();

        for (int i = 0; i < compCount; i++) {

            PeelComponent temp = ((PeelContainer)parent.getComponent() ).getChild(i);

            if (temp instanceof PeelContainer) {

                ListNode child = new ListNode(temp);
                addChildren(child);
                parent.add(child);
                assertHideAndLock( child );

            } else {
                ListNode node = new ListNode( temp );
                parent.add( node );
                assertHideAndLock( node );
            }
        }
    }

    private boolean removeComponentFromTree( PeelComponent component ) {

        ListNode node;
        Enumeration rootEnumeration = root.breadthFirstEnumeration();

        while (rootEnumeration.hasMoreElements()) {

            node = (ListNode)rootEnumeration.nextElement();

            if (node.getComponent() == component) {
                ListNode parent = (ListNode)node.getParent();
                node.removeFromParent();
                listModel.reload( parent );
                return true;
            }
        }
        return false;
    }

    private void hideAndLock() {

        ListNode node;
        Enumeration rootEnumeration = root.breadthFirstEnumeration();

        while (rootEnumeration.hasMoreElements()) {
            node = (ListNode)rootEnumeration.nextElement();

            boolean hideVal  = node.getHide();
            boolean compHide = node.getComponent().isHidden();

            boolean lockVal  = node.getLock();
            boolean compLock = node.getComponent().isLocked();

            if (hideVal != compHide || lockVal != compLock) {
                hideOrLock.add( node );
            }
        }
        change = ChangeProp.HIDE_LOCK;
    }

    private void assertHideAndLock( ListNode node ) {

        if (node.getHide() != node.getComponent().isHidden()) {
            node.setHide( node.getComponent().isHidden() );
        }

        if (node.getLock() != node.getComponent().isLocked()) {
            node.setLock( node.getComponent().isLocked() );
        }
    }

    protected void initListeners() {

        ListListener listener = new ListListener();

        outline.addMouseListener(listener);
        outline.addKeyListener(listener);
    }

    private class ListListener extends KeyAndMouseAdapter {

        public void mousePressed(MouseEvent e) {
            updateFromList = true;
        }

        public void mouseReleased(MouseEvent e) {
            updateHideLock = true;
        }

        public void keyPressed(KeyEvent e) {

            // Delete selected components from the workspace if the Delete key is pressed
            if (e.getKeyCode() == KeyEvent.VK_DELETE) {

                workbench.getBui().execute( new WorkBench.Task() {

                    public void run() {

                        // If there are no selections, there is nothing to delete
                        if ( workbench.getBui().getSelections().isEmpty() ) return;

                        // Add all the selection deletions into a MultiChange so they all appear/dissappear simultaneously
                        java.util.List<Change> deletes = new LinkedList< Change >();
                        for ( PeelComponent component : selectedComps ) {

                            if ( component.isLocked() ) continue;

                            if ( component instanceof PeelCanvasWindow) continue;

                            deletes.add( new Delete( component, workbench.getBui(), false ) );
                        }

                        if ( deletes.isEmpty() ) return;

                        MultiChange changes = new MultiChange( deletes );

                        changes.doChange();
                        workbench.pushChange( changes );
                        UpdateCont.get().newOrDeleteChange(true);
                    }
                });
            }
        }
    }
}